/*
 * Description: performance lifecycle handler
 */
#include "perf_lifecycle_handler.h"
#include <thread>
#include <algorithm>
#include <vector>
#include <unordered_map>
#include <chrono>
#include "Windows.h"
#include "log.h"
#include "common/guest_utils.h"
#include "performance/config/perf_config.h"
#include "performance/perf_constants.h"
#include "performance/perf_process_preload.h"
#include "performance/utils/perf_utils.h"
#include "performance/accelerator/acad_accelerator.h"
#include "performance/accelerator/defender_exclusion_accelerator.h"
#include "performance/accelerator/office_addins_accelerator.h"
#include "performance/accelerator/process_priority_accelerator.h"
#include "performance/accelerator/frequency_increasing_accelerator.h"

using namespace COMMON;

namespace PERF {

constexpr const auto DEFAULT_WAIT_SECONDS = 20;
constexpr const auto MS_EDGE_WAIT_SECONDS = 2;
constexpr const auto AUTOCAD_WAIT_SECONDS = 119;
constexpr const auto EXPLOERE_WAIT_SECONDS = 3;
constexpr const auto DELAY_WAIT_SECONDS = 2;

constexpr const auto PROCESS_LOAD_WAIT_PERIOD = 1000;
constexpr const auto MILLI_PER_SECOND = 1000;
constexpr const auto MILLI_PREVENT_EXCEED = 100;
constexpr const auto MAX_LEN_WND_TEXT = 256;

constexpr const auto WORD_WAIT_SECONDS = 19;
constexpr const auto EXCEL_WAIT_SECONDS = 19;
constexpr const auto PPT_WAIT_SECONDS = 19;
constexpr const auto OUTLOOK_WAIT_SECONDS = 17;
constexpr const auto XMIND_WAIT_SECONDS = 19;
constexpr const auto PS_WAIT_SECONDS = 19;
constexpr const auto ONENOTE_WAIT_SECONDS = 14;
constexpr const auto ZOOM_WAIT_SECONDS = 17;

const std::unordered_map<std::string, uint64_t> PNAME_TO_WAIT_SECONDS = { { PNAME_AUTOCAD, AUTOCAD_WAIT_SECONDS },
                                                                          { PNAME_MS_EDGE, MS_EDGE_WAIT_SECONDS },
                                                                          { PNAME_EXPLORER, EXPLOERE_WAIT_SECONDS },
                                                                          { PNAME_WORD, WORD_WAIT_SECONDS },
                                                                          { PNAME_EXCEL, EXCEL_WAIT_SECONDS },
                                                                          { PNAME_POWERPOINT, PPT_WAIT_SECONDS },
                                                                          { PNAME_ONENOTE, ONENOTE_WAIT_SECONDS },
                                                                          { PNAME_OUTLOOK, OUTLOOK_WAIT_SECONDS },
                                                                          { PNAME_XMIND, XMIND_WAIT_SECONDS },
                                                                          { PNAME_ZOOM, ZOOM_WAIT_SECONDS },
                                                                          { PNAME_PHOTOSHOP, PS_WAIT_SECONDS } };

bool PerfLifeCycleHandler::Init(std::shared_ptr<WINCONTROL::SendToHost> sendtoHost)
{
    accelerators.clear();
    if (PERF_CONFIG_OFFICE_ADDINS_LAZY_LOAD_ENABLED) {
        accelerators.push_back(std::make_shared<PERF::OfficeAddinsAccelerator>());
        LOG_INFO("[PERF - Init] - OfficeAddinsLazyLoad enabled");
    }
    if (PERF_CONFIG_FREQUENCY_INCREASING_ENABLED) {
        accelerators.push_back(std::make_shared<PERF::FrequencyIncreasingAccelerator>(sendtoHost));
        LOG_INFO("[PERF - Init] - FrequencyIncreasing enabled");
    }
    if (PERF_CONFIG_ADVENCED_PROCESS_PRIORITY_ENABLED) {
        accelerators.push_back(std::make_shared<PERF::ProcessPriorityAccelerator>());
        LOG_INFO("[PERF - Init] - AboveNormalProcessPriority enabled");
    }
    if (PERF_CONFIG_DYNAMIC_DEFENDER_EXCLUSION_ENABLED) {
        accelerators.push_back(std::make_shared<PERF::DefenderExclusionAccelerator>());
        LOG_INFO("[PERF - Init] - DynamicDefenderExclusion enabled");
    }
    if (PERF_CONFIG_ACAD_ACLR_ENABLED) {
        accelerators.push_back(std::make_shared<PERF::AcadAccelerator>());
        LOG_INFO("[PERF - Init] - AcadAccelerator enabled");
    }
    LOG_INFO("[PERF - Init] - {} accelerator(s) loaded", accelerators.size());
    return std::find(accelerators.begin(), accelerators.end(), nullptr) == accelerators.end();
}

void PerfLifeCycleHandler::Start(std::shared_ptr<PerfInfo> perfInfo)
{
    if (perfInfo == nullptr) {
        LOG_ERR("[PERF - Start] - perfInfo is null!");
        return;
    }
    std::thread tPerfLifeCycle([this, perfInfo]() {
        Prepare(perfInfo);
        OnProcessInitStart(perfInfo);
        StartProcessPreload(perfInfo);
        WaitProcessLoad(perfInfo);
        OnProcessInitEnd(perfInfo);
    });
    tPerfLifeCycle.detach();
}

void PerfLifeCycleHandler::Prepare(std::shared_ptr<PerfInfo> perfInfo)
{
    if (perfInfo == nullptr) {
        LOG_ERR("[PERF - Prepare] - perfInfo is null!");
        return;
    }
    auto pair = PNAME_TO_WAIT_SECONDS.find(perfInfo->appName);
    if (pair != PNAME_TO_WAIT_SECONDS.end()) {
        perfInfo->maxDuration = pair->second;
    } else {
        perfInfo->maxDuration = DEFAULT_WAIT_SECONDS;
    }
    perfInfo->maxDuration *= MILLI_PER_SECOND;
    perfInfo->maxDuration -= MILLI_PREVENT_EXCEED;
    perfInfo->startMilli =
        std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
            .count();
}

void PerfLifeCycleHandler::OnProcessInitStart(std::shared_ptr<PerfInfo> perfInfo)
{
    if (perfInfo == nullptr) {
        LOG_ERR("[PERF - OnProcessInitStart] - perfInfo is null!");
        return;
    }
    std::vector<std::shared_ptr<std::thread>> applyThreads;
    for (auto accelerator : accelerators) {
        applyThreads.push_back(
            std::make_shared<std::thread>([accelerator, perfInfo]() { accelerator->OnBegin(perfInfo); }));
    }
    for (auto pThread : applyThreads) {
        if (pThread->joinable()) {
            pThread->join();
        }
    }
}

void PerfLifeCycleHandler::OnProcessInitEnd(std::shared_ptr<PerfInfo> perfInfo)
{
    if (perfInfo == nullptr) {
        LOG_ERR("[PERF - OnProcessInitEnd] - perfInfo is null!");
        return;
    }
    std::vector<std::shared_ptr<std::thread>> disapplyThreads;
    for (auto accelerator : accelerators) {
        disapplyThreads.push_back(
            std::make_shared<std::thread>([accelerator, perfInfo]() { accelerator->OnEnd(perfInfo); }));
    }
    for (auto pThread : disapplyThreads) {
        if (pThread->joinable()) {
            pThread->join();
        }
    }
}

void PerfLifeCycleHandler::WaitProcessLoad(std::shared_ptr<PerfInfo> perfInfo)
{
    if (perfInfo == nullptr) {
        LOG_ERR("[PERF - WaitProcessLoad] - perfInfo is null!");
        return;
    }
    if (!CouldFetchLoadStatusFromWndText(perfInfo->appName)) {
        LOG_INFO("[PERF - WaitProcessLoad] - wait for {}ms", perfInfo->maxDuration);
        Sleep(perfInfo->maxDuration);
        return;
    }
    auto pnameToWndTextSuffixEntry = PNAME_TO_WND_TEXT_SUFFIX.find(perfInfo->appName);
    if (pnameToWndTextSuffixEntry == PNAME_TO_WND_TEXT_SUFFIX.end()) {
        return;
    }
    perfInfo->wndTextSuffix = pnameToWndTextSuffixEntry->second;
    perfInfo->wndTexts = FetchWndTexts(perfInfo->wndTextSuffix);
    bool isLoaded = false;
    while (!isLoaded) {
        isLoaded = TryFetchProcessLoadStatus(perfInfo);
        uint64_t crtMilli =
            std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
                .count();
        if (isLoaded) {
            if (crtMilli - perfInfo->startMilli <= perfInfo->maxDuration - DELAY_WAIT_SECONDS) {
                Sleep(DELAY_WAIT_SECONDS * MILLI_PER_SECOND);
            }
            return;
        }
        if (crtMilli - perfInfo->startMilli >= perfInfo->maxDuration - PROCESS_LOAD_WAIT_PERIOD) {
            return;
        }
        Sleep(PROCESS_LOAD_WAIT_PERIOD);
    }
}

bool PerfLifeCycleHandler::TryFetchProcessLoadStatus(std::shared_ptr<PerfInfo> perfInfo)
{
    if (perfInfo == nullptr) {
        LOG_ERR("[PERF - TryFetchProcessLoadStatus] - perfInfo is null!");
        return false;
    }
    std::vector<std::wstring> wndTexts = FetchWndTexts(perfInfo->wndTextSuffix);
    for (auto wndText : wndTexts) {
        if (std::find(perfInfo->wndTexts.begin(), perfInfo->wndTexts.end(), wndText) == perfInfo->wndTexts.end() &&
            wndText.find(perfInfo->wndTextSuffix) != std::wstring::npos) {
            return true;
        }
    }
    return false;
}

std::vector<std::wstring> ReadWndTexts(const std::wstring &wndTextSuffix)
{
    HANDLE hRead = CreateFile(WND_TEXT_PATH, GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
    if (hRead == INVALID_HANDLE_VALUE) {
        return {};
    }
    DWORD dataSize = GetFileSize(hRead, nullptr);
    DWORD dataReaded = 0;
    WCHAR *data = (WCHAR *)malloc(dataSize);
    if (data == nullptr) {
        CloseHandle(hRead);
        return {};
    }
    ZeroMemory(data, dataSize);
    WCHAR *tmpData = data;
    do {
        if (!ReadFile(hRead, tmpData, dataSize, &dataReaded, nullptr) || dataReaded == 0) {
            break;
        }
        dataSize -= dataReaded;
        tmpData += dataReaded;
    } while (dataSize != 0);
    CloseHandle(hRead);
    std::vector<std::wstring> wndTexts = PERF_UTILS.SplitUnicode(data, L"\n");
    std::vector<std::wstring>::iterator it = wndTexts.begin();
    while (it != wndTexts.end()) {
        if ((*it).find(wndTextSuffix) == std::wstring::npos) {
            it = wndTexts.erase(it);
        } else {
            it++;
        }
    }
    if (data != nullptr) {
        free(data);
        data = nullptr;
    }
    return wndTexts;
}

std::vector<std::wstring> PerfLifeCycleHandler::FetchWndTexts(const std::wstring &wndTextSuffix)
{
    DWORD exitCode;
    GuestUtils::ExecCmdWithExitCode("C:\\ProgramData\\performance\\wndtext_fetch_helper.exe", true, exitCode);
    std::vector<std::wstring> wndTexts = ReadWndTexts(wndTextSuffix);
    return wndTexts;
}

bool PerfLifeCycleHandler::CouldFetchLoadStatusFromWndText(const std::string &pname)
{
    return pname == PNAME_WORD || pname == PNAME_POWERPOINT || pname == PNAME_EXCEL;
}

bool PerfLifeCycleHandler::StartProcessPreload(std::shared_ptr<PerfInfo> perfInfo)
{
    if (perfInfo == nullptr) {
        LOG_ERR("[PERF] -StartProcessPreload perfInfo is null");
        return false;
    }
    LOG_INFO("[PERF] -StartProcessPreload enter");
    PERF_PROCESS_PRELOAD.ProcessPreloadEntry(
        perfInfo->appName, perfInfo->processId, perfInfo->cmdLine, perfInfo->appPath);
    return true;
}

}  // namespace PERF